package com.zn.opit.miniodemo.service.impl;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.google.common.collect.HashMultimap;
import com.zn.opit.miniodemo.config.ExtMinioClient;
import com.zn.opit.miniodemo.config.ExtMinioClientConfig;
import com.zn.opit.miniodemo.domain.minio.MinioUploadInfo;
import com.zn.opit.miniodemo.service.MinioService;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteObject;
import io.minio.messages.Part;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Component
@Slf4j
@RequiredArgsConstructor
public class MinioServiceImpl implements MinioService {

    @Value("${spring.application.name}")
    private String applicationName;

    @Resource
    private ExtMinioClient minioClient;

    @Resource
    private ExtMinioClientConfig minIoClientConfig;

    @Override
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse localUpload(String bucketName, String fileName, String filePath) {
        return minioClient.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .filename(filePath)
                        .build());
    }

    @Override
    public String upload(MultipartFile file) {
        String originalFilename = file.getOriginalFilename();
        if (StringUtils.isBlank(originalFilename)) {
            throw new RuntimeException();
        }
        String fileName = IdWorker.getIdStr() + originalFilename.substring(originalFilename.lastIndexOf("."));
        String objectName = applicationName + File.separator + fileName;
        try {
            PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(minIoClientConfig.getBucketName()).object(objectName)
                    .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
            //文件名称相同会覆盖
            minioClient.putObject(objectArgs);
        } catch (Exception e) {
            log.error("minio upload failed", e);
            return null;
        }
        return fileName;
    }

    @Override
    public String preview(String fileName) {
        // 查看文件地址
        fileName = applicationName + File.separator + fileName;
        GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs.builder()
                .bucket(minIoClientConfig.getBucketName()).object(fileName).method(Method.GET)
                .build();
        try {
            return minioClient.getPresignedObjectUrl(build);
        } catch (Exception e) {
            log.error("minio preview failed", e);
        }
        return null;
    }

    @Override
    public ResponseEntity<byte[]> download(String fileName, HttpServletResponse res) {
        fileName = applicationName + File.separator + fileName;
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(minIoClientConfig.getBucketName())
                .object(fileName).build();
        try (InputStream in = minioClient.getObject(objectArgs);
             FastByteArrayOutputStream out = new FastByteArrayOutputStream()) {
            IOUtils.copy(in, out);
            //封装返回值
            byte[] bytes = out.toByteArray();
            HttpHeaders headers = new HttpHeaders();
            try {
                headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                log.error("Unsupported Encoding", e);
            }
            headers.setContentLength(bytes.length);
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setAccessControlExposeHeaders(Collections.singletonList("*"));
            return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
        } catch (Exception e) {
            log.error("minio download failed", e);
        }
        return null;
    }

    @Override
    @SneakyThrows(Exception.class)
    public void deleteFile(String bucketName, String fileName) {
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }

    @Override
    @SneakyThrows(Exception.class)
    public void removeFiles(List<String> fileNames) {
        removeFiles(minIoClientConfig.getBucketName(), fileNames);
    }

    @Override
    @SneakyThrows(Exception.class)
    public void removeFiles(String bucketName, List<String> fileNames) {
        Stream<DeleteObject> stream = fileNames.stream().map(DeleteObject::new);
        minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(stream::iterator).build());
    }

    @Override
    @SneakyThrows(Exception.class)
    public List<Bucket> getAllBuckets() {
        return minioClient.listBuckets();
    }

    @Override
    public boolean existBucket(String name) {
        boolean exists;
        try {
            exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(name).build());
            if (!exists) {
                log.info("bucket [{}] not exist in minio, now create", name);
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
                log.info("create bucket [{}] success", name);
                exists = true;
            }
        } catch (Exception e) {
            log.error("minio bucket [{}] exist check failed", name, e);
            exists = false;
        }
        return exists;
    }

    @Override
    public Boolean makeBucket(String bucketName) {
        try {
            minioClient.makeBucket(MakeBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
        } catch (Exception e) {
            log.error("minio bucket [{}] create failed", bucketName, e);
            return false;
        }
        return true;
    }

    @Override
    @SneakyThrows(Exception.class)
    public Optional<Bucket> getBucket(String bucketName) {
        return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
    }

    @Override
    public Boolean removeBucket(String bucketName) {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
        } catch (Exception e) {
            log.error("minio bucket [{}] remove failed", bucketName, e);
            return false;
        }
        return true;
    }

    /**
     * 初始化获取 uploadId
     *
     * @param objectName  文件名
     * @param partCount   分片总数
     * @param contentType contentType
     */
    public MinioUploadInfo initMultiPartUpload(String objectName, int partCount, String contentType) {
        HashMultimap<String, String> headers = HashMultimap.create();
        headers.put(HttpHeaders.CONTENT_TYPE, contentType);

        String uploadId;
        List<String> partUrlList = new ArrayList<>();
        try {
            // 获取 uploadId
            uploadId = minioClient.getUploadId(minIoClientConfig.getBucketName(),
                    null,
                    objectName,
                    headers,
                    null);
            Map<String, String> paramsMap = new HashMap<>(2);
            paramsMap.put("uploadId", uploadId);
            for (int i = 1; i <= partCount; i++) {
                paramsMap.put("partNumber", String.valueOf(i));
                // 获取上传 url
                String uploadUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                        // 注意此处指定请求方法为 PUT，前端需对应，否则会报 `SignatureDoesNotMatch` 错误
                        .method(Method.PUT)
                        .bucket(minIoClientConfig.getBucketName())
                        .object(objectName)
                        // 指定上传连接有效期
                        .expiry(minIoClientConfig.getChunkUploadExpirySecond(), TimeUnit.SECONDS)
                        .extraQueryParams(paramsMap).build());

                partUrlList.add(uploadUrl);
            }
        } catch (Exception e) {
            log.error("initMultiPartUpload Error:" + e);
            return null;
        }
        MinioUploadInfo result = new MinioUploadInfo();
        result.setUploadId(uploadId);
        result.setExpiryTime(LocalDateTime.now()
                .minusSeconds(minIoClientConfig.getChunkUploadExpirySecond())
        ); // 过期时间
        result.setUploadUrls(partUrlList);
        return result;
    }

    /**
     * 分片合并
     *
     * @param objectName 文件名
     * @param uploadId   uploadId
     */
    public String mergeMultiPartUpload(String objectName, String uploadId) {
        ListPartsResponse partsResponse = listUploadPartsBase(objectName, uploadId);
        if (null == partsResponse) {
            log.error("查询文件分片列表为空");
            throw new RuntimeException("分片列表为空");
        }
        List<Part> partList = partsResponse.result().partList();
        Part[] parts = new Part[partList.size()];
        int partIndex = 0;
        for (Part partItem : partList) {
            parts[partIndex] = new Part(partIndex + 1, partItem.etag());
            partIndex++;
        }
        ObjectWriteResponse objectWriteResponse;
        try {
            objectWriteResponse = minioClient.mergeMultipart(minIoClientConfig.getBucketName(), null, objectName, uploadId, parts, null, null);
        } catch (Exception e) {
            log.error("分片合并失败：" + e);
            throw new RuntimeException("分片合并失败：" + e.getMessage());
        }
        if (null == objectWriteResponse) {
            log.error("合并失败，合并结果为空");
            throw new RuntimeException("分片合并失败");
        }
        return objectWriteResponse.region();
    }

    /**
     * 获取已上传的分片列表
     *
     * @param objectName 文件名
     * @param uploadId   uploadId
     */
    public List<Integer> listUploadChunkList(String objectName, String uploadId) {
        ListPartsResponse partsResponse = listUploadPartsBase(objectName, uploadId);
        if (null == partsResponse) {
            return Collections.emptyList();
        }
        return partsResponse.result().partList().stream()
                .map(Part::partNumber).collect(Collectors.toList());
    }


    private ListPartsResponse listUploadPartsBase(String objectName, String uploadId) {
        int maxParts = 1000;
        ListPartsResponse partsResponse;
        try {
            partsResponse = minioClient.listMultipart(minIoClientConfig.getBucketName(), null, objectName, maxParts, 0, uploadId, null, null);
        } catch (ServerException | InsufficientDataException | ErrorResponseException | NoSuchAlgorithmException |
                 IOException | XmlParserException | InvalidKeyException | InternalException |
                 InvalidResponseException e) {
            log.error("查询文件分片列表错误：{}，uploadId:{}", e, uploadId);
            return null;
        }
        return partsResponse;
    }

}
